package org.tigris.atlas.jmx.weblogic;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.management.InstanceNotFoundException;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import weblogic.management.Helper;
import weblogic.management.MBeanHome;
import weblogic.management.configuration.TargetMBean;

/**
 * This is the first pass at configuration via JMX.  It currently reads a proprties file to 
 * determine exactly how to setup WebLogic.  One might wonder why this class was written rather
 * when a set of existing ant tasks and wstl exists to do this job.  The ant task do not offer the 
 * felxibility as of WLS 8.1 to accomplish deployment in a repeatable manner.  For instance, they fail
 * by throwing a stack trace and if a resource already exists.  The WSTL is poorly doucmented and 
 * the nobody on the team should have to become a WSTL expert to perform deployment.  These java classes 
 * offer flexibility, a langauge the entire team understand and a JMX-standards baseline off which to work.
 * In the future, this functionality should be improved so that:
 * <ul>
 * <li>It reads XML configuration files rather than property files</li>
 * <li>Leverages standard JMX rather than Weblogic's extensions</li>
 * <li>This would ideally be packaged in a weblogic extension to core</li>
 * <li>Resources diffs should be performed to ensure that a resource is exactly as we expect it</li>
 * </ul>
 */
public class WeblogicConfiguration {

	static final String RECREATE_EXISTING_RESOURCES = "recreate.existing.resources";
	static final String WEBLOGIC_USERNAME = "weblogic.username";
	static final String WEBLOGIC_PASSWORD = "weblogic.password";
	static final String SERVER_URL = "server.url";
	static final String DEPLOYMENT_NAME = "deployment.name";	
	static final String JMS_SERVER_NAME = "jms.server.name";
	static final String JMS_JDBC_STORE_NAME = "jms.jdbc.store.name";
	static final String JMS_QUEUE_CONNECTION_FACTORY_NAME = "jms.queue.connection.factory.name";
	static final String DATASOURCE_NAME = "datasource.name";
	static final String CONNECTION_POOL_NAME = "connection.pool.name";
	
	static final String WLS_JMX_SERVER_TYPE = "Server";
	static final String WLS_JMX_CLUSTER_TYPE = "Cluster";
	
	/**
	 * Represents the logger for this class
	 */
	private Log log = LogFactory.getLog(WeblogicConfiguration.class);
	
	/**
	 * Allows this configuration tool to be executed from the command line.  It takes two parameters:
	 * <ul>
	 * <li>-c or --configLocation: The primary properties file from which to read</li>
	 * <li>-o or --overrideLocation: A secondary properties file that will be overlaid on top of the primary file</li> 
	 * </ul>
	 * @param args A <tt>String[]</tt> that contains the options describe above
	 */
	public static void main(String[] args) { 
		try {			
			CommandLineParser parser = new PosixParser();
			Options options = new Options();
			options.addOption("c", "configLocation", true, "The location of your configuration properties");
			options.addOption("o", "overrideLocation", true, "The location of your configuration OVERRIDE properties");
			CommandLine commandLine = parser.parse(options, args);			
			String configLocation = commandLine.getOptionValue("c");
			String overrideLocation = commandLine.getOptionValue("o");
			Option[] commandLineOptions = commandLine.getOptions();
			for (int i = 0; i < commandLineOptions.length; i++) {
				System.out.println("option: " + commandLineOptions[i].getOpt() + "=" + commandLineOptions[i].getValue());
			}
			
			
			WeblogicConfiguration wc = new WeblogicConfiguration();
			wc.configure(configLocation, overrideLocation);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Perfoms the configuration of a weblogic instance.  Once this class is refactored to accept XML-based
	 * configuration parameters, this method will be much more flexible.  At present, it perform the following 
	 * tasks in order:
	 * <ul>
	 * <li>Connects to WLS (cluster, server of both is determined by name from the deployment.name property)</li>
	 * <li>Create a JDBC Connection Pool</li>
	 * <li>Create Datasources</li>
	 * <li>Create JMS Connection Factories</li>
	 * <li>Create JMS Data Stores</li>
	 * <li>Create JMS Servers</li>
	 * </ul>
	 * Use a logging level of info for step-by-step configuration information or debug for property-level information.
	 * @param configLocation The location of the base configuration properties
	 * @param overrideLocation The location of the overlay configuration properties
	 * @throws Exception
	 */
	public void configure(String configLocation, String overrideLocation) throws Exception {
		if (log.isInfoEnabled()) {
			log.info("Beginning WebLogic configuration...");
		}
		
		Properties props = configureProperties(configLocation, overrideLocation);
		
		Object o = props.getProperty(RECREATE_EXISTING_RESOURCES);
		boolean recreateExistingResources = (o != null) ? new Boolean((String)o).booleanValue() : false;
		if (log.isInfoEnabled()) {
			log.info(RECREATE_EXISTING_RESOURCES + "=" + recreateExistingResources);
		}
		
		String weblogicUsername = getParameter(WEBLOGIC_USERNAME, props);
		String weblogicPassword = getParameter(WEBLOGIC_PASSWORD, props);
		String serverUrl = getParameter(SERVER_URL, props);
		String deploymentName = getParameter(DEPLOYMENT_NAME, props);		
		
		if (log.isInfoEnabled()) {
			log.info("Connecting to WebLogic with '" + serverUrl + "' as '" + weblogicUsername + "'.");
		}
		MBeanHome home = Helper.getAdminMBeanHome(weblogicUsername, weblogicPassword, serverUrl);
		if (log.isInfoEnabled()) {
			log.info("Connected.");
		}		
		
		TargetMBean[] targetServers = getTargets(deploymentName, home);		
		
		//create a JDBC Connection Pool:
		configureJDBCConnectionPools(props, recreateExistingResources, home, targetServers);
		
		//create datasources:
		configureDatasources(props, recreateExistingResources, home, targetServers);
		
		//create jms connection factories:
		configureJMSConnectionFactories(props, recreateExistingResources, home, targetServers);
		
		//create jms data stores:
		configureJMSDataStores(props, recreateExistingResources, home, targetServers);
		
		//create jms server:
		configureJMSServers(props, recreateExistingResources, home, targetServers);		
		
		if (log.isInfoEnabled()) {
			log.info("Completed WebLogic configuration.");
		}
	}

	private TargetMBean[] getTargets(String deploymentName, MBeanHome home) {
		if (deploymentName == null) {
			log.error("null deployment name encountered!");
			return null;
		}
		String targetName;	
		TargetMBean targetMBean = null;
		StringTokenizer st = new StringTokenizer(deploymentName, ",; ");
		int numberOfElements = st.countTokens();
		int targetServersIndex = 0;
		TargetMBean[] targetServers = new TargetMBean[numberOfElements];
		while ((numberOfElements > 0) && (st.hasMoreElements())) {
			//determine the king of deplyoment unit with which we are dealing:
			targetName = (String)st.nextElement();
			try {
				targetMBean = (TargetMBean)home.getAdminMBean(targetName, WLS_JMX_SERVER_TYPE);
				if (log.isInfoEnabled()) {
					log.info("Targeting to server '" + targetName + "'");
				}
			} catch (InstanceNotFoundException infe) {
				//do nothing - this is expected in some cases
			}
			
			if (targetMBean == null) {
				try {
					targetMBean = (TargetMBean)home.getAdminMBean(targetName, WLS_JMX_CLUSTER_TYPE);
					if (log.isInfoEnabled()) {
						log.info("Targeting to cluster '" + targetName + "'");
					}
					
				} catch (InstanceNotFoundException infe) {
					log.error("The target '" + targetName + "' is neither a Server or Cluster!");
					int newLength = targetServers.length - 1;					
					TargetMBean[] newArray = new TargetMBean[newLength];
					for (int i = 0; i < newLength; i++) {
						newArray[i] = targetServers[i];
					}
					targetServers = newArray;
				}
			}
			
			if (targetMBean != null) {
				targetServers[targetServersIndex++] = targetMBean;
			}
		}
		return targetServers;
	}

	private Properties configureProperties(String configLocation, String overrideLocation) throws IOException {
		Properties props = loadProperties(configLocation, null);
		if (log.isInfoEnabled()) {
			log.info("Loaded configuration properties.");
		}
		if (overrideLocation != null) {
			props = loadProperties(overrideLocation, props);
			if (log.isInfoEnabled()) {
				log.info("Loaded override properties.");
			}
		}
		if (log.isInfoEnabled()) {
			log.info("Loaded " + props.size() + " external properties...");
		}
		return props;
	}

	private void configureJMSServers(Properties props, boolean recreateExistingResources, MBeanHome home, TargetMBean[] targetServers) {
		String jmsServerName = props.getProperty(JMS_SERVER_NAME);
		JMSServerConfiguration jmsServerConfig = new JMSServerConfiguration(jmsServerName, props);
		jmsServerConfig.configure(home, targetServers, recreateExistingResources);
	}

	private void configureJMSDataStores(Properties props, boolean recreateExistingResources, MBeanHome home, TargetMBean[] targetServers) {
		String jmsJdbcStoreName = props.getProperty(JMS_JDBC_STORE_NAME);
		JMSJDBCStoreConfiguration jmsJdbcStoreConfig = new JMSJDBCStoreConfiguration(jmsJdbcStoreName, props);
		jmsJdbcStoreConfig.configure(home, targetServers, recreateExistingResources);
	}

	private void configureJMSConnectionFactories(Properties props, boolean recreateExistingResources, MBeanHome home, TargetMBean[] targetServers) {
		String jmsConnectionFactoryName = props.getProperty(JMS_QUEUE_CONNECTION_FACTORY_NAME);
		JMSConnectionFactoryConfiguration jmsConnectionFactoryConfig = 
			new JMSConnectionFactoryConfiguration(jmsConnectionFactoryName, props);
		jmsConnectionFactoryConfig.configure(home, targetServers, recreateExistingResources);
	}

	private void configureDatasources(Properties props, boolean recreateExistingResources, MBeanHome home, TargetMBean[] targetServers) {
		String datasourceName = props.getProperty(DATASOURCE_NAME);
		JDBCTxDatasourceConfiguration jdbcDatasourceConfig = new JDBCTxDatasourceConfiguration(datasourceName, props);
		jdbcDatasourceConfig.configure(home, targetServers, recreateExistingResources);
	}

	private void configureJDBCConnectionPools(Properties props, boolean recreateExistingResources, MBeanHome home, TargetMBean[] targetServers) {
		String jdbcConnectionPoolName = props.getProperty(CONNECTION_POOL_NAME);
		JDBCConnectionPoolConfiguation jdbcConnectionPoolConfig = 
			new JDBCConnectionPoolConfiguation(jdbcConnectionPoolName, props);
		jdbcConnectionPoolConfig.configure(home, targetServers, recreateExistingResources);
	}

	private Properties loadProperties(String location, Properties defaultProps) throws IOException {
		Properties props = (defaultProps != null) ? defaultProps : new Properties();
	
		if (location == null) {
			if (log.isErrorEnabled()) {
				log.error("The location can not be null!");
			}
		} else {			
			File configFile = new File(location);
			boolean fileExists = configFile.exists();
			if (fileExists) {
				try {
					props.load(new FileInputStream(location));
				} catch (FileNotFoundException fnfe) {
					fileExists = false;
				}
			} 
			
			if (!fileExists) {
				if (log.isErrorEnabled()) {
					log.error("The location can not be found! - " + configFile.getCanonicalPath());
				}
			}
		}
		
		return props;
	}
	
	private String getParameter(String parameterName, Properties properties) {
		String parameterValue = properties.getProperty(parameterName);
		if (StringUtils.isBlank(parameterValue)) {			
			if (log.isErrorEnabled()) {
				log.error("Parameter '" + parameterName + "' is required for a configuring Weblogic!");
			}			
		}
		
		return parameterValue;
	}
	
}
